/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.riotfamily.common.web.cache.annotation;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.security.Principal;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.quartz.CronExpression;
import org.riotfamily.cachius.CacheContext;
import org.riotfamily.cachius.CacheService;
import org.riotfamily.cachius.http.AbstractHttpHandler;
import org.riotfamily.common.util.ExceptionUtils;
import org.riotfamily.common.util.FormatUtils;
import org.riotfamily.common.util.Generics;
import org.riotfamily.common.web.cache.CacheKeyAugmentor;
import org.riotfamily.common.web.mvc.view.ViewResolverHelper;
import org.riotfamily.common.web.support.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;
/**
* Subclass of the {@link AnnotationMethodHandlerAdapter} that supports the
* {@link Cache} annotation.
* <p>
* By default, the cacheKey is automatically constructed from the URL and the
* arguments passed to the handler method:
* <p>
* <code><i><request-url></i><b>#</b><i><method-name></i><b>@RequestMapping(</b><i><mapping></i><b>) {</b><i><method-args></i><b>}</b></code>
* <p>
* ... where <i><method-args></i> is a list of the String-representations
* of all {@link #isSupportedArgument(Annotation[], Class) supported} handlerMethod arguments (separated by semicolons).
* <p>
* In case the argument list contains an unsupported argument, you have to provide a method
* that manually constructs the <i><method-args></i> part. To do so, implement
* a method with the following signature:
* <p>
* <code><b>public</b> <i><? extends CharSequence></i> getCacheKeyFor<i><method-name></i><b>(</b><i><method-args></i><b>)</b></code>
* <p>
* ... where the argument-list must be exactly the same as for the handler-method.
* <p>
* The same mechanism can be used to provide a last-modified date for a handler-method:
* <p>
* <code><b>public</b> long getLastModifiedFor<i><method-name></i><b>(</b><i><method-args></i><b>)</b></code>
*/
@SuppressWarnings("unchecked")
public class CacheAnnotationHandlerAdapter extends AnnotationMethodHandlerAdapter
implements Ordered, InitializingBean {
Logger log = LoggerFactory.getLogger(CacheAnnotationHandlerAdapter.class);
private CacheService cacheService;
CacheKeyAugmentor cacheKeyAugmentor;
ViewResolverHelper viewResolverHelper;
private int order = 0;
private Set<Class<? extends Annotation>> ignoredAnnotations;
private Set<Class<? extends Annotation>> supportedAnnotations;
private Set<Class<?>> ignoredTypes;
private Set<Class<?>> supportedTypes;
public CacheAnnotationHandlerAdapter(CacheService cacheService,
CacheKeyAugmentor cacheKeyAugmentor) {
this.cacheService = cacheService;
this.cacheKeyAugmentor = cacheKeyAugmentor;
}
/**
* Returns the order in which this HandlerAdapter is processed.
*/
@Override
public int getOrder() {
return this.order;
}
/**
* Set the order in which this HandlerAdapter is processed.
*/
@Override
public void setOrder(int order) {
this.order = order;
}
public void setSupportedAnnotations(Set<Class<? extends Annotation>> supportedAnnotations) {
this.supportedAnnotations = supportedAnnotations;
}
public void setSupportedTypes(Set<Class<?>> supportedTypes) {
this.supportedTypes = supportedTypes;
}
public void setIgnoredAnnotations(Set<Class<? extends Annotation>> ignoredAnnotations) {
this.ignoredAnnotations = ignoredAnnotations;
}
public void setIgnoredTypes(Set<Class<?>> ignoredTypes) {
this.ignoredTypes = ignoredTypes;
}
public void afterPropertiesSet() throws Exception {
if (ignoredAnnotations == null) {
ignoredAnnotations = Generics.newHashSet();
}
if (supportedAnnotations == null) {
supportedAnnotations = Generics.newHashSet();
}
if (ignoredTypes == null) {
ignoredTypes = Generics.newHashSet();
}
if (supportedTypes == null) {
supportedTypes = Generics.newHashSet();
}
ignoredAnnotations.add(PathVariable.class);
supportedAnnotations.addAll(Arrays.asList(
RequestParam.class, RequestHeader.class, CookieValue.class));
ignoredTypes.addAll(Arrays.asList(
Model.class, ModelMap.class, Map.class, Errors.class,
BindingResult.class, OutputStream.class, Writer.class,
HttpServletResponse.class));
supportedTypes.addAll(Arrays.asList(
Locale.class, Principal.class));
}
@Override
protected void initApplicationContext() throws BeansException {
viewResolverHelper = new ViewResolverHelper(getApplicationContext());
}
@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
cacheService.handle(new AnnotationCacheHandler(request, response, handler));
return null;
}
public ModelAndView doHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
return super.handle(request, response, handler);
}
protected String getDefaultCacheKeyPrefix(HttpServletRequest request, Method handlerMethod) {
return ServletUtils.getOriginatingRequestUrl(request)
.append('#').append(handlerMethod.getName())
.append('@').append(StringUtils.unqualify(handlerMethod.getAnnotation(RequestMapping.class).toString()))
.toString();
}
protected CharSequence getDefaultMethodLevelCacheKey(Method handlerMethod, Object[] args) {
StringBuilder key = new StringBuilder();
key.append(" {");
Class<?>[] types = handlerMethod.getParameterTypes();
Annotation[][] ann = handlerMethod.getParameterAnnotations();
for (int i = 0; i < types.length; i++) {
if (isSupportedArgument(ann[i], types[i])) {
key.append(String.valueOf(args[i])).append(';');
}
}
key.append('}');
return key;
}
protected boolean isSupportedArgument(Annotation[] annotations, Class<?> type) {
return !containsIgnoredAnnotation(annotations) && (containsSupportedAnnotation(annotations) || isSupportedType(type));
}
private boolean containsSupportedAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (isSupportedAnnotation(annotation)) {
return true;
}
}
return false;
}
private boolean containsIgnoredAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (isIgnoredAnnotation(annotation)) {
return true;
}
}
return false;
}
private boolean isSupportedAnnotation(Annotation annotation) {
if (supportedAnnotations.contains(annotation.annotationType())) {
return true;
}
else if (!isIgnoredAnnotation(annotation)) {
throw new IllegalStateException("Unsupported annotation: " + annotation);
}
return false;
}
private boolean isIgnoredAnnotation(Annotation annotation) {
return ignoredAnnotations.contains(annotation.annotationType());
}
private boolean isSupportedType(Class<?> type) {
if (supportedTypes.contains(type)) {
return true;
}
if (!ignoredTypes.contains(type)) {
throw new IllegalStateException("Unsupported parameter type: " + type);
}
return false;
}
// ----------------------------------------------------------------------
private class AnnotationCacheHandler extends AbstractHttpHandler {
private Object handler;
private Method handlerMethod;
private Object[] args;
private Cache annotation;
private Method lastModifiedMethod;
public AnnotationCacheHandler(HttpServletRequest request,
HttpServletResponse response, Object handler) {
super(request, response);
this.handler = handler;
init();
}
private void init() {
ProxyFactory proxyFactory = new ProxyFactory(handler);
proxyFactory.setProxyTargetClass(true);
HandlerMethodInterceptor interceptor = new HandlerMethodInterceptor();
proxyFactory.addAdvice(interceptor);
try {
invokeHandlerMethod(getRequest(), getResponse(), proxyFactory.getProxy());
MethodInvocation invocation = interceptor.getInvocation();
handlerMethod = invocation.getMethod();
args = invocation.getArguments();
annotation = handlerMethod.getAnnotation(Cache.class);
String name = "getLastModifiedFor" + StringUtils.capitalize(handlerMethod.getName());
lastModifiedMethod = ReflectionUtils.findMethod(handler.getClass(), name, handlerMethod.getParameterTypes());
}
catch (Exception e) {
throw ExceptionUtils.wrapReflectionException(e);
}
}
@Override
public String getCacheRegion() {
CacheRegion region = handler.getClass().getAnnotation(CacheRegion.class);
return region != null ? region.value() : null;
}
@Override
public long getLastModified() {
try {
if (lastModifiedMethod != null) {
Assert.isAssignable(Long.TYPE, lastModifiedMethod.getReturnType());
return (Long) lastModifiedMethod.invoke(handler, args);
}
return System.currentTimeMillis();
}
catch (Exception e) {
throw ExceptionUtils.wrapReflectionException(e);
}
}
@Override
protected boolean isCompressible() {
return annotation.gzip();
}
@Override
public String getCacheKey() {
if (annotation == null) {
return null;
}
try {
Method method = ReflectionUtils.findMethod(handler.getClass(),
"getCacheKey", HttpServletRequest.class, Method.class);
CharSequence prefix;
if (method != null) {
Assert.isAssignable(CharSequence.class, method.getReturnType());
prefix = (CharSequence) method.invoke(handler, getRequest(), handlerMethod);
}
else {
prefix = getDefaultCacheKeyPrefix(getRequest(), handlerMethod);
}
if (prefix == null) {
return null;
}
CharSequence suffix;
String name = "getCacheKeyFor" + StringUtils.capitalize(handlerMethod.getName());
method = ReflectionUtils.findMethod(handler.getClass(), name, handlerMethod.getParameterTypes());
if (method != null) {
Assert.isAssignable(CharSequence.class, method.getReturnType());
suffix = (CharSequence) method.invoke(handler, args);
}
else {
suffix = getMethodLevelCacheKey();
}
if (suffix == null) {
return null;
}
StringBuilder key = new StringBuilder(prefix).append(suffix);
if (cacheKeyAugmentor != null) {
cacheKeyAugmentor.augmentCacheKey(key, getRequest());
}
return key.toString();
}
catch (Exception e) {
throw ExceptionUtils.wrapReflectionException(e);
}
}
private CharSequence getMethodLevelCacheKey() {
String name = "getCacheKeyFor" + StringUtils.capitalize(handlerMethod.getName());
Method method = ReflectionUtils.findMethod(handler.getClass(), name, handlerMethod.getParameterTypes());
if (method != null) {
Assert.isAssignable(CharSequence.class, method.getReturnType());
return (CharSequence) ReflectionUtils.invokeMethod(method, handler, args);
}
return getDefaultMethodLevelCacheKey(handlerMethod, args);
}
@Override
protected void handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
applyContextSettings();
ModelAndView mv = doHandle(request, response, handler);
if (mv != null) {
View view = viewResolverHelper.resolveView(getRequest(), mv);
view.render(mv.getModel(), getRequest(), response);
}
}
private void applyContextSettings() throws ParseException {
if (annotation != null) {
if (annotation.serveStaleOnError()) {
CacheContext.serveStaleOnError();
}
if (annotation.serveStaleUntilExpired()) {
CacheContext.serveStaleUntilExpired();
}
if (annotation.serveStaleWhileRevalidate()) {
CacheContext.serveStaleWhileRevalidate();
}
Long expireIn = null;
if (StringUtils.hasText(annotation.ttl())) {
expireIn = FormatUtils.parseMillis(annotation.ttl());
}
if (StringUtils.hasText(annotation.cron())) {
CronExpression cron = new CronExpression(annotation.cron());
long delta = cron.getNextValidTimeAfter(new Date()).getTime()
- System.currentTimeMillis();
if (expireIn == null || delta < expireIn) {
expireIn = delta;
}
}
if (expireIn != null) {
CacheContext.expireIn(expireIn);
}
else if (lastModifiedMethod != null) {
CacheContext.expireIn(0);
}
}
}
}
private static class HandlerMethodInterceptor implements MethodInterceptor {
private MethodInvocation invocation;
public Object invoke(MethodInvocation invocation) throws Throwable {
this.invocation = invocation;
return null;
}
public MethodInvocation getInvocation() {
return invocation;
}
}
}